#pragma once

//TODO:
//  - Sutherland-Hodgeman clipping against Z = 0

#include "vertex_buffer_software.h"
#include "index_buffer_software.h"
#include "fixed_point_helper.h"
#include "matrix.h"
#include "jobs.h"

namespace krryn{
namespace system_imp{
	template<typename _Vertex, typename _Type>
	struct render_job_data{
		typedef scene::vertex_buffer<system::software_tag, _Vertex> vb;
		typedef scene::index_buffer<system::software_tag> ib;
		typedef triangle_renderer_software<_Vertex, _Type> rt;

		typedef typename vb::ptr vb_ptr;
		typedef typename ib::ptr ib_ptr;


		vb_ptr m_VertexBuffer;
		ib_ptr m_IndexBuffer;

		rt &m_TriangleRenderer;
		math::matrix m_WorldToClip;

		view::canvas *m_Canvas;
		texture_software *m_Texture;

		render_job_data(vb_ptr a_VertexBuffer, ib_ptr a_IndexBuffer, rt &a_TriangleRenderer)
			: m_VertexBuffer(a_VertexBuffer), m_IndexBuffer(a_IndexBuffer), 
			  m_TriangleRenderer(a_TriangleRenderer) {}
	};

	template<typename _Vertex, typename _Type>
	class render_job{
	public:
		typedef scene::vertex_buffer<system::software_tag, _Vertex> vb;
		typedef scene::index_buffer<system::software_tag> ib;

		typedef typename _Vertex::description_type description_type;

		typedef typename vb::const_iterator vb_iter;
		typedef typename ib::const_iterator ib_iter;

		enum{ X = 0, Y = 1, Z = 2, W = 3 };
		
		// You'll see this occasionally, that's because we have to account for the extra W coordinate stored by the vertex buffer
		enum{ Size = 1 + description_type::m_Size};

		enum{ A = 0, B = Size, C = 2 * Size };

		render_job(const render_job_data<_Vertex, _Type> &a_Data) 
			: m_Data(a_Data), m_HalfWidth(-1), m_HalfHeight(-1)
		{
			m_Root = new triangle();

		}
		
		_Type m_Matrix[16];

		virtual void execute(){
			init();

			convert_matrix(m_Matrix);
			extract_frustum_planes(m_Matrix);

			ib_iter l_Index    = m_Data.m_IndexBuffer->begin();
			ib_iter l_IndexEnd = m_Data.m_IndexBuffer->end();
			vb_iter l_Vertex   = m_Data.m_VertexBuffer->begin();

			for(; l_Index != l_IndexEnd; l_Index += 3){
				copy_to_root(
					l_Vertex + l_Index[0] * Size, 
					l_Vertex + l_Index[1] * Size,
					l_Vertex + l_Index[2] * Size);

				clip_frustum(m_Root);

				//to_clip_space(m_Root, 0, l_Matrix);
				//to_clip_space(m_Root, 1, l_Matrix);
				//to_clip_space(m_Root, 2, l_Matrix);

				//bool l_TriviallyAccept = true;
				//bool l_TriviallyReject = true;

				///// TODO: perform clipping
				//for(int i = 0; i < 3; i++){
				//	for(int j = 0; j < 3; j++){
				//		// test each vertex of the triangle against each
				//		// of the clipping planes. We're checking in clip
				//		// space so all the clipping planes are at -w and w e.g.
				//		// (0, 0, -w) or (w, 0, 0) et cetera.

				//		int k = i * Size + j;
				//		_Type l_W = m_Root->m_Vtx[i * Size + W];

				//		l_TriviallyAccept &= m_Root->m_Vtx[k] >= -l_W;
				//		l_TriviallyReject &= m_Root->m_Vtx[k]  <  l_W;
				//	}
				//}

				//if(l_TriviallyAccept){
				//	project_and_draw(m_Root);
				//}else{
				//	/// clip the triangle using sutherland-hodgman triangle 
				//	/// clipping. Perform triangulation of the generated
				//	/// polygon at the same time using the triangle struct.
				//	/// allocate data as needed (just grow that linked list
				//	/// dynamically) but never de-allocate. We'll do that
				//	/// in the destructor once we're done.

				//	/// DONT FORGET: when interpolating the vertices of the 
				//	/// triangle when an intersection is found, don't forget W

				//	clip_frustum(m_Root);
				//}
			}
		}
	private:
		template<typename _Type, size_t _Size>
		struct triangle_t{
			_Type m_Vtx[3 * _Size]; // vertices

			triangle_t() : m_Next(0) {}

			triangle_t *m_Next;
		};

		typedef triangle_t<_Type, Size> triangle;

		_Type m_Frustum[6][4];

		void extract_frustum_planes(_Type *a_Matrix){
			// Left clipping plane    
			m_Frustum[0][0] = m_Matrix[12] + m_Matrix[0];
			m_Frustum[0][1] = m_Matrix[13] + m_Matrix[1];
			m_Frustum[0][2] = m_Matrix[14] + m_Matrix[2];
			m_Frustum[0][3] = m_Matrix[15] + m_Matrix[3];

			// Right clipping plane
			m_Frustum[1][0] = m_Matrix[12] - m_Matrix[0];
			m_Frustum[1][1] = m_Matrix[13] - m_Matrix[1];
			m_Frustum[1][2] = m_Matrix[14] - m_Matrix[2];
			m_Frustum[1][3] = m_Matrix[15] - m_Matrix[3];

			// Top clipping plane
			m_Frustum[2][0] = m_Matrix[12] - m_Matrix[4];
			m_Frustum[2][1] = m_Matrix[13] - m_Matrix[5];
			m_Frustum[2][2] = m_Matrix[14] - m_Matrix[6];
			m_Frustum[2][3] = m_Matrix[15] - m_Matrix[7];

			// Bottom clipping plane
			m_Frustum[3][0] = m_Matrix[12] + m_Matrix[4];
			m_Frustum[3][1] = m_Matrix[13] + m_Matrix[5];
			m_Frustum[3][2] = m_Matrix[14] + m_Matrix[6];
			m_Frustum[3][3] = m_Matrix[15] + m_Matrix[7];

			// Near clipping plane
			m_Frustum[4][0] = m_Matrix[12] + m_Matrix[8];
			m_Frustum[4][1] = m_Matrix[13] + m_Matrix[9];
			m_Frustum[4][2] = m_Matrix[14] + m_Matrix[10];
			m_Frustum[4][3] = m_Matrix[15] + m_Matrix[11];

			// Far clipping plane
			m_Frustum[5][0] = m_Matrix[12] - m_Matrix[8];
			m_Frustum[5][1] = m_Matrix[13] - m_Matrix[9];
			m_Frustum[5][2] = m_Matrix[14] - m_Matrix[10];
			m_Frustum[5][3] = m_Matrix[15] - m_Matrix[11];
		}

		void extract_frustum_planes_1(_Type *a_Matrix){
			// Left clipping plane
			m_Frustum[0][0] = a_Matrix[3] + a_Matrix[0];
			m_Frustum[0][1] = a_Matrix[7] + a_Matrix[5];
			m_Frustum[0][2] = a_Matrix[11] + a_Matrix[8];
			m_Frustum[0][3] = a_Matrix[15] + a_Matrix[12];

			// Right clipping plane
			m_Frustum[1][0] = a_Matrix[3] - a_Matrix[0];
			m_Frustum[1][1] = a_Matrix[7] - a_Matrix[5];
			m_Frustum[1][2] = a_Matrix[11] - a_Matrix[8];
			m_Frustum[1][3] = a_Matrix[15] - a_Matrix[12];

			// Top clipping plane
			m_Frustum[2][0] = a_Matrix[3] - a_Matrix[1];
			m_Frustum[2][1] = a_Matrix[7] - a_Matrix[5];
			m_Frustum[2][2] = a_Matrix[11] - a_Matrix[9];
			m_Frustum[2][3] = a_Matrix[15] - a_Matrix[13];

			// Bottom clipping plane
			m_Frustum[3][0] = a_Matrix[3] + a_Matrix[1];
			m_Frustum[3][1] = a_Matrix[7] + a_Matrix[5];
			m_Frustum[3][2] = a_Matrix[11] + a_Matrix[9];
			m_Frustum[3][3] = a_Matrix[15] + a_Matrix[13];

			// Near clipping plane
			m_Frustum[4][0] = a_Matrix[2];
			m_Frustum[4][1] = a_Matrix[6];
			m_Frustum[4][2] = a_Matrix[10];
			m_Frustum[4][3] = a_Matrix[14];

			// Far clipping plane
			m_Frustum[5][0] = a_Matrix[3] - a_Matrix[2];
			m_Frustum[5][1] = a_Matrix[7] - a_Matrix[6];
			m_Frustum[5][2] = a_Matrix[11] - a_Matrix[10];
			m_Frustum[5][3] = a_Matrix[15] - a_Matrix[14];
		}

		void clip_frustum(triangle *a_Tri){
			_Type l_TmpVtx1[36 * Size] = {};
			_Type l_TmpVtx2[36 * Size] = {};

			int l_Size = 3;

			l_Size = clip_to_plane(a_Tri->m_Vtx, l_TmpVtx2, l_Size, m_Frustum[2]);
			l_Size = clip_to_plane(l_TmpVtx2, l_TmpVtx1, l_Size, m_Frustum[1]);
			l_Size = clip_to_plane(l_TmpVtx1, l_TmpVtx2, l_Size, m_Frustum[4]);

			l_Size = clip_to_plane(l_TmpVtx2, l_TmpVtx1, l_Size, m_Frustum[3]);
			l_Size = clip_to_plane(l_TmpVtx1, l_TmpVtx2, l_Size, m_Frustum[0]);
			l_Size = clip_to_plane(l_TmpVtx2, l_TmpVtx1, l_Size, m_Frustum[5]);

			/// resulting data in l_TmpVtx1

			for(int i = 2; i < l_Size; i++){
				copy_to_tri(
					m_Root,
					l_TmpVtx1,
					l_TmpVtx1 + ((i - 1) * Size),
					l_TmpVtx1 + (i * Size));
				
				project_and_draw(m_Root);
			}
		}

		int clip_to_plane(_Type *a_Src, _Type *a_Dst, int a_Size, _Type *a_Plane){
			int l_Count = 0;
			int l_Index = 0;

			_Type *l_Current = a_Src;

			for(int i = 1; i < a_Size + 1; i++){
				_Type *l_Next = a_Src + (i % a_Size) * Size;

				_Type l_DistNext	= distance(a_Plane, l_Next);
				_Type l_DistCurrent = distance(a_Plane, l_Current);
				_Type l_Delta       = l_DistCurrent - l_DistNext;

				bool l_GoingInNext  = l_DistNext > 0;
				bool l_CrossingEdge = l_GoingInNext != l_DistCurrent > 0;

				if(l_CrossingEdge){
					// we can only calculate the ratio here to prevent a divide 
					// by zero error if l_Delta = 0 (that case is defined as not
					// crossing an edge).
					_Type l_Ratio = div<_Type>(l_DistCurrent, l_Delta);

					output_interpolated(a_Dst, l_Index, l_Next, l_Current, l_Ratio);
					l_Count++;
				}

				if(l_GoingInNext){
					output(a_Dst, l_Next, l_Index);
					l_Count++;
				}

				l_Current = l_Next;
			}

			return l_Count;
		}

		void output(_Type *a_Dest, _Type *a_Src, int &a_Index){
			memcpy(a_Dest + a_Index, a_Src, sizeof(_Type) * Size);
			a_Index += Size;
		}

		void output_interpolated(_Type *a_Dest, int &a_Index, _Type *a_Start, _Type *a_End, _Type a_Ratio){
			for(int i = 0; i < Size; i++){
				a_Dest[a_Index + i] = lerp<_Type>(a_Start[i], a_End[i], a_Ratio);
			}

			a_Index += Size;
		}

		void init(){
			m_Data.m_TriangleRenderer.set_canvas(m_Data.m_Canvas);
			m_Data.m_TriangleRenderer.set_texture(m_Data.m_Texture);

			// the size of the canvas isn't know before this point, so we calculate it here
			m_HalfWidth  = cast_to<_Type>(.5f * (float)m_Data.m_Canvas->get_width());
			m_HalfHeight = cast_to<_Type>(.5f * (float)m_Data.m_Canvas->get_height());
		}

		void convert_matrix(_Type *a_Matrix){
			for(int i = 0; i < 16; i++){
				a_Matrix[i] = cast_to<_Type>(m_Data.m_WorldToClip.cell[i]);
			}
		}

		void copy_to_root(const _Type *a_V1, const _Type *a_V2, const _Type *a_V3){
			copy_to_tri(m_Root, a_V1, a_V2, a_V3);
		}

		void copy_to_tri(triangle *a_Triangle, const _Type *a_V1, const _Type *a_V2, const _Type *a_V3){
			const size_t l_Size = sizeof(_Type) * Size;

			memcpy(a_Triangle->m_Vtx + A, a_V1, l_Size);
			memcpy(a_Triangle->m_Vtx + B, a_V2, l_Size);
			memcpy(a_Triangle->m_Vtx + C, a_V3, l_Size);
		}

		template<typename _Type>
		void to_clip_space(triangle *a_Tri, int a_N, _Type *a_Matrix){
			// a_Position is assumed to be of length 3, w is assumed to be 1
			// a_Matrix is assumed to be of length 16, and row-major

			int l_N = a_N * Size;

			/// transform vertex to clip-space (matrix multiplication)
			_Type l_X = dot(a_Matrix +  0, a_Tri->m_Vtx + l_N);
			_Type l_Y = dot(a_Matrix +  4, a_Tri->m_Vtx + l_N);
			_Type l_Z = dot(a_Matrix +  8, a_Tri->m_Vtx + l_N);
			_Type l_W = dot(a_Matrix + 12, a_Tri->m_Vtx + l_N);

			a_Tri->m_Vtx[l_N + X] = l_X;
			a_Tri->m_Vtx[l_N + Y] = l_Y;
			a_Tri->m_Vtx[l_N + Z] = l_Z;
			a_Tri->m_Vtx[l_N + W] = l_W;
		}

		_Type distance(_Type *a_Plane, _Type *a_Position){
			// a_Row is assumed to be 4
			// a_Column is assumed to be 4

			return 
				  mul<_Type>(a_Plane[X], a_Position[X])
				+ mul<_Type>(a_Plane[Y], a_Position[Y])
				+ mul<_Type>(a_Plane[Z], a_Position[Z])
				+ mul<_Type>(a_Plane[W], a_Position[W]);
		}

		_Type dot(_Type *a_Row, _Type *a_Colum){
			// a_Row is assumed to be 4
			// a_Column is assumed to be 3

			return 
				  mul<_Type>(a_Row[X], a_Colum[X])
				+ mul<_Type>(a_Row[Y], a_Colum[Y])
				+ mul<_Type>(a_Row[Z], a_Colum[Z])
				+ a_Row[W];
		}

		void project_and_draw(triangle *a_Tri){
			to_clip_space(m_Root, 0, m_Matrix);
			to_clip_space(m_Root, 1, m_Matrix);
			to_clip_space(m_Root, 2, m_Matrix);

			to_screen_space(a_Tri->m_Vtx + A);
			to_screen_space(a_Tri->m_Vtx + B);
			to_screen_space(a_Tri->m_Vtx + C);

			//if(is_front_facing(a_Tri))
			{
				m_Data.m_TriangleRenderer.draw(
					a_Tri->m_Vtx + A, 
					a_Tri->m_Vtx + B, 
					a_Tri->m_Vtx + C
				);
			}
		}

		void to_screen_space(_Type *a_Position){
			/// perform Perspective Divide to get Normalized Device Coordinates
			a_Position[X] = div<_Type>(a_Position[X], a_Position[W]);
			a_Position[Y] = div<_Type>(a_Position[Y], a_Position[W]);
			a_Position[Z] = div<_Type>(a_Position[Z], a_Position[W]);
			a_Position[W] = fp_values<_Type>::one();

			/// convert NDC to Screen Space

			/// x = w * (.5 * x + .5)
			a_Position[X] = 
				  mul<_Type>(m_HalfWidth, a_Position[X]) 
				+ m_HalfWidth;

			/// y = h * (.5 * y + .5)
			a_Position[Y] = 
				  m_HalfHeight 
				- mul<_Type>(m_HalfHeight, a_Position[Y]) - 1;

			/// z = .5 * z + .5
			a_Position[Z] = 
				  mul<_Type>(fp_values<_Type>::half(), a_Position[Z]) 
				+ fp_values<_Type>::half();
		}

		bool is_front_facing(triangle *a_Tri){
			/// back-face culling check in screen space

			/// Calculating this earlier would've required us to compute a
			/// normal, a cross product and a dot product. This is just 2
			/// multiplications and an add.

			return fp_values<_Type>::zero() >= det_2x2<_Type>(
				a_Tri->m_Vtx[C + X] - a_Tri->m_Vtx[A + X],
				a_Tri->m_Vtx[C + Y] - a_Tri->m_Vtx[A + Y],
				a_Tri->m_Vtx[B + X] - a_Tri->m_Vtx[A + X],
				a_Tri->m_Vtx[B + Y] - a_Tri->m_Vtx[A + Y]
			);
		}

		triangle *m_Root;

		_Type m_HalfWidth;
		_Type m_HalfHeight;

		render_job_data<_Vertex, _Type> m_Data;
	};
}
}
